qutebrowser Completer

qutebrowser 作为一个命令驱动的浏览器,良好的补全体验非常关键。在 qutebrowser 中,补全模块的实现比较复杂,即包含 GUI 视图绘制,也包含补全逻辑。因此 qutebrowser 对视图、逻辑进行了拆分,Completer 专门负责补全模块的纯逻辑部分的实现。

_partition

方法的主要目的是将命令行文本分割成多个部分,并围绕光标位置进行分割。

这么说有些抽象,结合使用来看。当用户在命令模式下输入命令,此时是 Completer 介入的时机,为用户提供补全提示。

原理分析

以以下命令输入为例:

Screenshot from 2023-12-29 14-50-50.png

其中包含两部分内容:

  1. 命令内容:即 :open -t mmm
  2. 光标位置:用户可以移动光标,进行行内编辑

回到 _partition,它的作用是根据命令内容和光标位置,对用户输入进行切分。

在我打字输入过程中,_partition 输入输出如下:

用户操作 方法输入(切分后) 方法三元组输出
输入 o ['o'] [] 'o' []
输入 p ['op'] [] 'op' []
输入 e ['ope'] [] 'ope' []
输入 n ['open'] [] 'open' []
输入 ['open', ' '] ['open'] '' []
输入 - ['open', ' -'] ['open'] '-' []
输入 t ['open', ' -t'] ['open'] '-t' []
输入 ['open', ' -t', ' '] ['open', '-t'] '' []
输入 m ['open', ' -t', ' m'] ['open', '-t'] 'm' []
输入 m ['open', ' -t', ' mm'] ['open', '-t'] 'mm' []
输入 m ['open', ' -t', ' mmm'] ['open', '-t'] 'mmm' []
左方向键 ['open', ' -t', ' mmm'] ['open', '-t'] 'mmm' []
左方向键 ['open', ' -t', ' mmm'] ['open', '-t'] 'mmm' []
左方向键 ['open', ' -t', ' mmm'] ['open', '-t'] 'mmm' []
左方向键 ['open', ' -t', ' mmm'] ['open'] '-t' ['mmm']
左方向键 ['open', ' -t', ' mmm'] ['open'] '-t' ['mmm']

以上过程,包含逐字输入,输入完成后,左移光标的过程。

_partition 返回一个元组,包含三个元素:光标前的部分,光标下的部分,光标后的部分。

结合拆解推演过程,对这个结果有了直观理解。

在函数实现上,有几个细节值得关注:

首先,在命令切分上,首先尝试使用 CommandParser 执行切分,如果命令不存在,则降级为 split 分割:

# 使用 CommandParser 解析文本,如果命令不存在,则降级为 split 分割
try:
	parse_result = parser.CommandParser().parse(text, keep=True)
except cmdexc.NoSuchCommandError:
	cmdline = split.split(text, keep=True)
else:
	cmdline = parse_result.cmdline

_partition 的最核心逻辑,是计算当前光标落在哪个命令部分上,具体实现为:获取当前光标据开头的总距离,从头开始,以每个命令长度削减总距离,如果在某个部分总长度被减为负数,这个部分变为当前部分。之前、之后部分基于此便可获得。具体实现:

# 获取光标位置,并确保光标位置不超过文本长度
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text))  # Qt treats 2-byte UTF-16 chars as 2 chars
# ……
# 它遍历 parts 中的每一部分
for i, part in enumerate(parts):
	# 消耗总长度
	pos -= len(part)
	# 找到当前部分,计算前后部分
	if pos <= 0:
		if part[pos-1:pos+1].isspace():
			# cursor is in a space between two existing words
			parts.insert(i, '')
		prefix = [x.strip() for x in parts[:i]]
		center = parts[i].strip()
		# strip trailing whitespace included as a separate token
		postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
		return prefix, center, postfix

raise utils.Unreachable(f"Not all parts consumed: {parts}")

何处被使用

Command(输入命令的 EditText 组件)cursorPositionChanged 信号和 textChanged

还有其他调用路径,略。

_get_new_completion

主要目的是根据当前的命令文本获取一个新的补全函数。它接收两个参数:光标前的命令块(before_cursor)和光标下的命令块(under_cursor)。

还是以 :open -t mmm 为例,当我在逐字录入时,主观感受:

这个交互体验细节非常酷。

代码实现如下:

def _get_new_completion(self, before_cursor, under_cursor):
	# ...
    if not before_cursor:
        log.completion.debug('Starting command completion')
        print('Starting command completion')
        # 首次进入命令模式,列出所有命令的补全
        return miscmodels.command

	# 尝试根据命令获取输入
    try:
        cmd = objects.commands[before_cursor[0]]
    except KeyError:
        log.completion.debug("No completion for unknown command: {}"
                                .format(before_cursor[0]))
        return None

    # ...
    argpos = len(before_cursor) - 1
    try:
	    # 根据命令参数类型,获取对应的补全函数
        func = cmd.get_pos_arg_info(argpos).completion
    except IndexError:
        log.completion.debug("No completion in position {}".format(argpos))
        return None
    # 返回新的补全函数
    return func

本文作者:Maeiee

本文链接:qutebrowser Completer

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!